We can either use JWT or opaque tokens. We prefer JWT for horizontally scaled systems or distributed systems or if we want to avoid database lookups, and chose opaque tokens if instant logout, instant revocation, session tracking, device management
Users: Table or collection for storing user information
Sessions/Tokens: Table or collection to store session /token infromation
Store a SHA-256 hash of the refresh token, never the token itself. The actual token is only given to the client in the cookie.
We already saw middleware protecting /account and /checkout. But for fine‑grained role‑based access, you can also check in Server Components or in the layout, because middleware doesn’t have access to full request body and you may need DB data.
Middleware handles broad redirection (unauthenticated → login).
Server Components check roles and show 403 or redirect if needed.
Client Components can conditionally render UI using useAuth().
Passwords: hash with bcrypt (cost ≥ 12). Never store plaintext.
Cookies: httpOnly, Secure, SameSite=Lax. Path=/. For extra protection against CSRF, you can add a custom request header validation.
Refresh token rotation: each time a refresh token is used, issue a new one and invalidate the old. This limits the window if a token is stolen.
Rate limiting: protect login/server actions with a rate limiter (e.g., using Upstash Ratelimit or a custom in‑memory store). Prevents brute‑force.
Token expiry: keep access token short (5‑15 min). Refresh token can be longer (7‑30 days), but must be revocable.
CORS & CSP: properly set Content Security Policy headers to mitigate XSS. Since we use httpOnly cookies, XSS cannot directly steal tokens, but still guard your app.
Secure session termination: on password change or suspicious activity, delete all refresh tokens for that user.